Merge branch 'master' of https://github.com/metersphere/metersphere
This commit is contained in:
commit
f377bd251a
|
@ -6,11 +6,11 @@ ARG MS_VERSION=dev
|
|||
|
||||
RUN mkdir -p /opt/apps && mkdir -p /opt/jmeter/lib/junit
|
||||
|
||||
COPY backend/target/backend-1.5.jar /opt/apps
|
||||
COPY backend/target/backend-1.6.jar /opt/apps
|
||||
|
||||
COPY backend/target/classes/jmeter/ /opt/jmeter/
|
||||
|
||||
ENV JAVA_APP_JAR=/opt/apps/backend-1.5.jar
|
||||
ENV JAVA_APP_JAR=/opt/apps/backend-1.6.jar
|
||||
|
||||
ENV AB_OFF=true
|
||||
|
||||
|
|
15
README.md
15
README.md
|
@ -13,10 +13,10 @@
|
|||
|
||||
MeterSphere 是一站式开源持续测试平台,涵盖测试跟踪、接口测试、性能测试、团队协作等功能,兼容JMeter 等开源标准,有效助力开发和测试团队充分利用云弹性进行高度可扩展的自动化测试,加速高质量软件的交付。
|
||||
|
||||
- 测试跟踪: 远超 TestLink 的使用体验;
|
||||
- 接口测试: 类似 Postman 的体验;
|
||||
- 测试跟踪: 远超 TestLink 的使用体验,覆盖从编写用例到生成测试报告的完整流程;
|
||||
- 接口测试: 集 Postman 的易用与 JMeter 的灵活于一体,接口管理、多协议支持、场景自动化,你想要的全都有;
|
||||
- 性能测试: 兼容 JMeter,支持 Kubernetes 和云环境,轻松支持高并发、分布式的性能测试;
|
||||
- 团队协作: 两级租户体系,天然支持团队协作。
|
||||
- 团队协作: 用户管理、租户管理、权限管理、资源管理,无论团队规模如何,总有适合的落地方式。
|
||||
|
||||
![产品定位](https://metersphere.oss-cn-hangzhou.aliyuncs.com/img/ct-devops.png)
|
||||
|
||||
|
@ -72,12 +72,11 @@ v1.1.0 是 v1.0.0 之后的功能版本。
|
|||
|
||||
像其它优秀开源项目一样,MeterSphere 将每月发布一个功能版本。
|
||||
|
||||
## 技术优势
|
||||
## 产品优势
|
||||
|
||||
- 全生命周期: 能够覆盖从测试计划到测试执行、测试报告分析的不同阶段;
|
||||
- 自动化 & 扩展性: 支持接口和性能的自动化测试,可以充分利用云弹性实现超大规模的性能测试;
|
||||
- 持续测试: 能够与持续集成工具无缝集成,支撑企业实现测试左移;
|
||||
- 团队协作: 支持不同规模的测试团队,小到几个人的测试团队、大到数百人的测试中心。
|
||||
- 开源:基于开源、兼容开源;按月发布新版本、日均下载安装超过100次、被大量客户验证;
|
||||
- 一站式:一个产品全面涵盖测试跟踪、接口测试、性能测试等功能并形成联动:其中用例管理是底座需求、接口自动化测试是高频需求、性能测试是专家服务为主工具为辅;一个产品全满足从测试计划、测试执行到测试报告分析的全生命周期需求;
|
||||
- 持续测试:能将测试融入持续交付和 DevOps 体系;无缝对接 Bug 管理工具和持续集成工具等;支持团队协作和资产沉淀。
|
||||
|
||||
## 功能列表
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>metersphere-server</artifactId>
|
||||
<groupId>io.metersphere</groupId>
|
||||
<version>1.5</version>
|
||||
<version>1.6</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
|
|
@ -51,6 +51,7 @@ public class APITestController {
|
|||
public Pager<List<APITestResult>> list(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryAPITestRequest request) {
|
||||
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
|
||||
request.setWorkspaceId(SessionUtils.getCurrentWorkspaceId());
|
||||
request.setProjectId(SessionUtils.getCurrentProjectId());
|
||||
return PageUtils.setPageInfo(page, apiTestService.list(request));
|
||||
}
|
||||
|
||||
|
|
|
@ -34,8 +34,8 @@ public class ApiAutomationController {
|
|||
}
|
||||
|
||||
@PostMapping(value = "/create")
|
||||
public void create(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
|
||||
apiAutomationService.create(request, bodyFiles);
|
||||
public ApiScenario create(@RequestPart("request") SaveApiScenarioRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
|
||||
return apiAutomationService.create(request, bodyFiles);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/update")
|
||||
|
|
|
@ -67,6 +67,7 @@ public class MsAssertions extends MsTestElement {
|
|||
assertion.setProperty(TestElement.TEST_CLASS, ResponseAssertion.class.getName());
|
||||
assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("AssertionGui"));
|
||||
assertion.setAssumeSuccess(assertionRegex.isAssumeSuccess());
|
||||
assertion.addTestString(assertionRegex.getExpression());
|
||||
assertion.setToContainsType();
|
||||
switch (assertionRegex.getSubject()) {
|
||||
case "Response Code":
|
||||
|
|
|
@ -68,7 +68,6 @@ public class APITestService {
|
|||
|
||||
public List<APITestResult> list(QueryAPITestRequest request) {
|
||||
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
|
||||
request.setProjectId(SessionUtils.getCurrentProjectId());
|
||||
return extApiTestMapper.list(request);
|
||||
}
|
||||
|
||||
|
|
|
@ -100,11 +100,12 @@ public class ApiAutomationService {
|
|||
apiScenarioMapper.deleteByExample(example);
|
||||
}
|
||||
|
||||
public void create(SaveApiScenarioRequest request, List<MultipartFile> bodyFiles) {
|
||||
public ApiScenario create(SaveApiScenarioRequest request, List<MultipartFile> bodyFiles) {
|
||||
request.setId(UUID.randomUUID().toString());
|
||||
checkNameExist(request);
|
||||
|
||||
|
||||
final ApiScenario scenario = new ApiScenario();
|
||||
scenario.setId(UUID.randomUUID().toString());
|
||||
scenario.setId(request.getId());
|
||||
scenario.setName(request.getName());
|
||||
scenario.setProjectId(request.getProjectId());
|
||||
scenario.setTagId(request.getTagId());
|
||||
|
@ -132,6 +133,7 @@ public class ApiAutomationService {
|
|||
|
||||
List<String> bodyUploadIds = request.getBodyUploadIds();
|
||||
apiDefinitionService.createBodyFiles(bodyUploadIds, bodyFiles);
|
||||
return scenario;
|
||||
}
|
||||
|
||||
public void update(SaveApiScenarioRequest request, List<MultipartFile> bodyFiles) {
|
||||
|
@ -248,7 +250,7 @@ public class ApiAutomationService {
|
|||
JSONObject element = JSON.parseObject(item.getScenarioDefinition());
|
||||
MsScenario scenario = JSONObject.parseObject(item.getScenarioDefinition(), MsScenario.class);
|
||||
// 多态JSON普通转换会丢失内容,需要通过 ObjectMapper 获取
|
||||
if (element!= null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
|
||||
if (element != null && StringUtils.isNotEmpty(element.getString("hashTree"))) {
|
||||
LinkedList<MsTestElement> elements = mapper.readValue(element.getString("hashTree"),
|
||||
new TypeReference<LinkedList<MsTestElement>>() {
|
||||
});
|
||||
|
|
|
@ -166,7 +166,9 @@ public class ApiModuleService extends NodeTreeService<ApiModuleDTO> {
|
|||
ApiModuleDTO nodeTree = request.getNodeTree();
|
||||
|
||||
List<ApiModule> updateNodes = new ArrayList<>();
|
||||
|
||||
if (nodeTree == null) {
|
||||
return;
|
||||
}
|
||||
buildUpdateDefinition(nodeTree, apiModule, updateNodes, "/", "0", nodeTree.getLevel());
|
||||
|
||||
updateNodes = updateNodes.stream()
|
||||
|
@ -179,8 +181,7 @@ public class ApiModuleService extends NodeTreeService<ApiModuleDTO> {
|
|||
}
|
||||
|
||||
private void buildUpdateDefinition(ApiModuleDTO rootNode, List<ApiDefinitionResult> apiDefinitions,
|
||||
List<ApiModule> updateNodes, String rootPath, String pId, int level) {
|
||||
|
||||
List<ApiModule> updateNodes, String rootPath, String pId, int level) {
|
||||
rootPath = rootPath + rootNode.getName();
|
||||
|
||||
if (level > 8) {
|
||||
|
|
|
@ -13,7 +13,6 @@ import io.metersphere.base.mapper.ApiScenarioModuleMapper;
|
|||
import io.metersphere.base.mapper.ext.ExtApiScenarioModuleMapper;
|
||||
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.service.NodeTreeService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -24,7 +23,10 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
|
@ -140,7 +142,9 @@ public class ApiScenarioModuleService extends NodeTreeService<ApiScenarioModuleD
|
|||
ApiScenarioModuleDTO nodeTree = request.getNodeTree();
|
||||
|
||||
List<ApiScenarioModule> updateNodes = new ArrayList<>();
|
||||
|
||||
if (nodeTree == null) {
|
||||
return;
|
||||
}
|
||||
buildUpdateDefinition(nodeTree, apiScenarios, updateNodes, "/", "0", nodeTree.getLevel());
|
||||
|
||||
updateNodes = updateNodes.stream()
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
|
||||
<select id="selectMaxResultByResourceId" parameterType="java.lang.String" resultType="io.metersphere.base.domain.ApiDefinitionExecResult">
|
||||
select * from api_definition_exec_result
|
||||
where resource_id = #{resourceId,jdbcType=VARCHAR} ORDER BY update_time DESC LIMIT 1
|
||||
where resource_id = #{resourceId,jdbcType=VARCHAR} ORDER BY start_time DESC LIMIT 1
|
||||
</select>
|
||||
</mapper>
|
|
@ -1 +1 @@
|
|||
Subproject commit bb494fc68a2367359c9048fa7250c7618de4afb6
|
||||
Subproject commit 61397c16728a63493507679f7e0940d9099f337f
|
|
@ -7,7 +7,7 @@
|
|||
<parent>
|
||||
<artifactId>metersphere-server</artifactId>
|
||||
<groupId>io.metersphere</groupId>
|
||||
<version>1.5</version>
|
||||
<version>1.6</version>
|
||||
</parent>
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
|
|
@ -240,7 +240,7 @@
|
|||
});
|
||||
},
|
||||
copy(row) {
|
||||
row.id = getUUID();
|
||||
row.copy = true;
|
||||
this.$emit('edit', row);
|
||||
},
|
||||
showReport(row) {
|
||||
|
|
|
@ -493,6 +493,9 @@
|
|||
request.enable === undefined ? request.enable = true : request.enable;
|
||||
request.active = false;
|
||||
request.resourceId = getUUID();
|
||||
if (!request.url) {
|
||||
request.url = "";
|
||||
}
|
||||
if (referenced === 'REF' || !request.hashTree) {
|
||||
request.hashTree = [];
|
||||
}
|
||||
|
@ -557,8 +560,8 @@
|
|||
copyRow(row, node) {
|
||||
const parent = node.parent
|
||||
const hashTree = parent.data.hashTree || parent.data;
|
||||
let obj = {};
|
||||
Object.assign(obj, row);
|
||||
// 深度复制
|
||||
let obj = JSON.parse(JSON.stringify(row));
|
||||
obj.resourceId = getUUID();
|
||||
hashTree.push(obj);
|
||||
this.sort();
|
||||
|
@ -604,7 +607,7 @@
|
|||
this.getEnvironments();
|
||||
},
|
||||
allowDrop(draggingNode, dropNode, type) {
|
||||
if (ELEMENTS.get(dropNode.data.type).indexOf(draggingNode.data.type) != -1) {
|
||||
if (dropNode.data.type === draggingNode.data.type || ELEMENTS.get(dropNode.data.type).indexOf(draggingNode.data.type) != -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -694,9 +697,12 @@
|
|||
if (valid) {
|
||||
this.setParameter();
|
||||
let bodyFiles = this.getBodyUploadFiles(this.currentScenario);
|
||||
this.$fileUpload(this.path, null, bodyFiles, this.currentScenario, () => {
|
||||
this.$fileUpload(this.path, null, bodyFiles, this.currentScenario, response => {
|
||||
this.$success(this.$t('commons.save_success'));
|
||||
this.path = "/api/automation/update";
|
||||
if (response.data) {
|
||||
this.currentScenario.id = response.data.id;
|
||||
}
|
||||
this.currentScenario.tagId = JSON.parse(this.currentScenario.tagId);
|
||||
this.$emit('refresh');
|
||||
})
|
||||
|
@ -719,6 +725,9 @@
|
|||
this.scenarioDefinition = obj.hashTree;
|
||||
}
|
||||
}
|
||||
if (this.currentScenario.copy) {
|
||||
this.path = "/api/automation/create";
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
if (Object.prototype.toString.call(this.currentApi.response).match(/\[object (\w+)\]/)[1].toLowerCase() === 'object') {
|
||||
this.response = this.currentApi.response;
|
||||
} else {
|
||||
this.response = new ResponseFactory(JSON.parse(this.currentApi.response));
|
||||
this.response = JSON.parse(this.currentApi.response);
|
||||
}
|
||||
} else {
|
||||
this.response = {headers: [], body: new Body(), statusCode: [], type: "HTTP"};
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<el-form-item :label="$t('commons.password')" prop="password" v-if=" authConfig.verification!=undefined && authConfig.verification !='No Auth'">
|
||||
<el-input v-model="authConfig.password" :placeholder="$t('commons.password')" show-password autocomplete="off"
|
||||
maxlength="20" show-word-limit/>
|
||||
maxlength="50" show-word-limit/>
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
method: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
|
||||
url: [{required: true, message: this.$t('api_test.definition.request.path_all_info'), trigger: 'blur'}],
|
||||
},
|
||||
debugForm: {method: REQ_METHOD[0].id},
|
||||
debugForm: {method: REQ_METHOD[0].id, environmentId: ""},
|
||||
options: [],
|
||||
responseData: {type: 'HTTP', responseResult: {}, subRequestResults: []},
|
||||
loading: false,
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
{name: this.$t('api_test.definition.request.batch_edit'), handleClick: this.handleEditBatch}
|
||||
],
|
||||
typeArr: [
|
||||
{id: 'status', name: this.$t('api_test.definition.api_case_status')},
|
||||
{id: 'status', name: this.$t('api_test.definition.api_status')},
|
||||
{id: 'method', name: this.$t('api_test.definition.api_type')},
|
||||
{id: 'userId', name: this.$t('api_test.definition.api_principal')},
|
||||
],
|
||||
|
|
|
@ -167,8 +167,7 @@
|
|||
this.reload();
|
||||
},
|
||||
copyRow(row) {
|
||||
let obj = {};
|
||||
Object.assign(obj, row);
|
||||
let obj =JSON.parse(JSON.stringify(row));
|
||||
obj.id = getUUID();
|
||||
this.request.hashTree.push(obj);
|
||||
this.reload();
|
||||
|
|
|
@ -141,8 +141,7 @@
|
|||
this.reload();
|
||||
},
|
||||
copyRow(row) {
|
||||
let obj = {};
|
||||
Object.assign(obj, row);
|
||||
let obj =JSON.parse(JSON.stringify(row));
|
||||
obj.id = getUUID();
|
||||
this.request.hashTree.push(obj);
|
||||
this.reload();
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-row style="margin: 20px">
|
||||
<span style="margin-right: 10px">
|
||||
{{$t('api_test.request.connect_timeout')}}:
|
||||
</span>
|
||||
<span style="margin-right: 10px">
|
||||
<el-input-number size="small" :disabled="isReadOnly" v-model="request.connectTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
|
||||
</span>
|
||||
<span style="margin-right: 10px">
|
||||
{{$t('api_test.request.response_timeout')}}:
|
||||
</span>
|
||||
<span style="margin-right: 10px">
|
||||
<el-input-number size="small" :disabled="isReadOnly" v-model="request.responseTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
|
||||
</span>
|
||||
</el-row>
|
||||
<el-row style="margin: 20px">
|
||||
<span style="margin-right: 10px">
|
||||
<el-checkbox class="follow-redirects-item" v-model="request.followRedirects">{{$t('api_test.request.follow_redirects')}}</el-checkbox>
|
||||
</span>
|
||||
<span style="margin-right: 10px">
|
||||
<el-checkbox class="do-multipart-post" v-model="request.doMultipartPost">{{$t('api_test.request.do_multipart_post')}}</el-checkbox>
|
||||
</span>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MsApiAdvancedConfig",
|
||||
props: {
|
||||
request: Object,
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
|
@ -60,6 +60,10 @@
|
|||
<ms-api-auth-config :is-read-only="isReadOnly" :request="request"/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="其他设置" name="advancedConfig">
|
||||
<ms-api-advanced-config :is-read-only="isReadOnly" :request="request"/>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div v-if="!referenced">
|
||||
|
@ -101,13 +105,13 @@
|
|||
import {REQUEST_HEADERS} from "@/common/js/constants";
|
||||
import MsApiVariable from "../../ApiVariable";
|
||||
import MsJsr233Processor from "../../processor/Jsr233Processor";
|
||||
import MsApiAdvancedConfig from "../../ApiAdvancedConfig";
|
||||
import {createComponent} from "../../jmeter/components";
|
||||
import MsApiAssertions from "../../assertion/ApiAssertions";
|
||||
import MsApiExtract from "../../extract/ApiExtract";
|
||||
import {Assertions, Body, Extract, KeyValue} from "../../../model/ApiTestModel";
|
||||
import {getUUID} from "@/common/js/utils";
|
||||
import BatchAddParameter from "../../basis/BatchAddParameter";
|
||||
import MsApiAdvancedConfig from "./ApiAdvancedConfig";
|
||||
|
||||
|
||||
export default {
|
||||
|
@ -200,8 +204,7 @@
|
|||
this.reload();
|
||||
},
|
||||
copyRow(row) {
|
||||
let obj = {};
|
||||
Object.assign(obj, row);
|
||||
let obj =JSON.parse(JSON.stringify(row));
|
||||
obj.id = getUUID();
|
||||
this.request.hashTree.push(obj);
|
||||
this.reload();
|
||||
|
|
|
@ -195,8 +195,7 @@
|
|||
this.reload();
|
||||
},
|
||||
copyRow(row) {
|
||||
let obj = {};
|
||||
Object.assign(obj, row);
|
||||
let obj =JSON.parse(JSON.stringify(row));
|
||||
obj.id = getUUID();
|
||||
this.request.hashTree.push(obj);
|
||||
this.reload();
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit a22a3005d9bd254793fcf634d72539cbdf31be3a
|
||||
Subproject commit d39dafaf84b9c7a56cb51f2caf67dd7dfde5938c
|
Loading…
Reference in New Issue