Merge branch 'v1.1'

This commit is contained in:
chenjianxing 2020-07-30 18:45:14 +08:00
commit 1b0dc296fc
19 changed files with 198 additions and 45 deletions

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AssertionJsonPath extends AssertionType {
private String expect;
private String expression;
private String description;
public AssertionJsonPath() {
setType(AssertionType.JSON_PATH);
}
}

View File

@ -6,6 +6,7 @@ import lombok.Data;
public class AssertionType { public class AssertionType {
public final static String REGEX = "Regex"; public final static String REGEX = "Regex";
public final static String DURATION = "Duration"; public final static String DURATION = "Duration";
public final static String JSON_PATH = "JSONPath";
public final static String TEXT = "Text"; public final static String TEXT = "Text";
private String type; private String type;

View File

@ -7,5 +7,6 @@ import java.util.List;
@Data @Data
public class Assertions { public class Assertions {
private List<AssertionRegex> regex; private List<AssertionRegex> regex;
private List<AssertionJsonPath> jsonPath;
private AssertionDuration duration; private AssertionDuration duration;
} }

View File

@ -1,6 +1,7 @@
package io.metersphere.api.jmeter; package io.metersphere.api.jmeter;
import io.metersphere.commons.exception.MSException; import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.config.JmeterProperties; import io.metersphere.config.JmeterProperties;
import io.metersphere.i18n.Translator; import io.metersphere.i18n.Translator;
import org.apache.jmeter.config.Arguments; import org.apache.jmeter.config.Arguments;
@ -33,6 +34,7 @@ public class JMeterService {
LocalRunner runner = new LocalRunner(testPlan); LocalRunner runner = new LocalRunner(testPlan);
runner.run(); runner.run();
} catch (Exception e) { } catch (Exception e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(Translator.get("api_load_script_error")); MSException.throwException(Translator.get("api_load_script_error"));
} }
} }

View File

@ -225,6 +225,7 @@
</foreach> </foreach>
</if> </if>
</where> </where>
order by update_time desc
</select> </select>
<select id="getMaxNumByProjectId" resultType="io.metersphere.base.domain.TestCase"> <select id="getMaxNumByProjectId" resultType="io.metersphere.base.domain.TestCase">

View File

@ -358,7 +358,6 @@ public class TestCaseService {
private List<TestCaseExcelData> generateTestCaseExcel(TestCaseBatchRequest request) { private List<TestCaseExcelData> generateTestCaseExcel(TestCaseBatchRequest request) {
List<TestCaseDTO> TestCaseList = extTestCaseMapper.listByTestCaseIds(request); List<TestCaseDTO> TestCaseList = extTestCaseMapper.listByTestCaseIds(request);
List<TestCaseExcelData> list = new ArrayList<>(); List<TestCaseExcelData> list = new ArrayList<>();
SessionUser user = SessionUtils.getUser();
StringBuilder step = new StringBuilder(""); StringBuilder step = new StringBuilder("");
StringBuilder result = new StringBuilder(""); StringBuilder result = new StringBuilder("");
TestCaseList.forEach(t -> { TestCaseList.forEach(t -> {
@ -395,18 +394,7 @@ public class TestCaseService {
} }
data.setMaintainer(t.getMaintainer()); data.setMaintainer(t.getMaintainer());
list.add(data); list.add(data);
}); });
list.add(new TestCaseExcelData());
TestCaseExcelData explain = new TestCaseExcelData();
explain.setName(Translator.get("do_not_modify_header_order"));
explain.setNodePath(Translator.get("module_created_automatically"));
explain.setType(Translator.get("options") + "functional、performance、api");
explain.setMethod(Translator.get("options") + "manual、auto");
explain.setPriority(Translator.get("options") + "P0、P1、P2、P3");
explain.setMaintainer(Translator.get("please_input_workspace_member"));
list.add(explain);
return list; return list;
} }

View File

@ -0,0 +1,93 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col>
<el-input :disabled="isReadOnly" v-model="jsonPath.expression" maxlength="200" size="small" show-word-limit
:placeholder="$t('api_test.request.extract.json_path_expression')"/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="jsonPath.expect" maxlength="2000" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expect')"/>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {JSONPath} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionJsonPath",
props: {
jsonPath: {
type: JSONPath,
default: () => {
return new JSONPath();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
watch: {
'jsonPath.expect'() {
this.setJSONPathDescription();
},
'jsonPath.expression'() {
this.setJSONPathDescription();
}
},
methods: {
add: function () {
this.list.push(this.getJSONPath());
this.callback();
},
remove: function () {
this.list.splice(this.index, 1);
},
getJSONPath() {
let jsonPath = new JSONPath(this.jsonPath);
jsonPath.description = jsonPath.expression + " expect: " + (jsonPath.expect ? jsonPath.expect : '');
return jsonPath;
},
setJSONPathDescription() {
this.jsonPath.description = this.jsonPath.expression + " expect: " + (this.jsonPath.expect ? this.jsonPath.expect : '');
}
}
}
</script>
<style scoped>
.assertion-select {
width: 250px;
}
.assertion-item {
width: 100%;
}
.assertion-btn {
text-align: center;
width: 60px;
}
</style>

View File

@ -10,7 +10,7 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col> <el-col>
<el-input :disabled="isReadOnly" v-model="regex.expression" maxlength="255" size="small" show-word-limit <el-input :disabled="isReadOnly" v-model="regex.expression" maxlength="2000" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expression')"/> :placeholder="$t('api_test.request.assertions.expression')"/>
</el-col> </el-col>
<el-col class="assertion-btn"> <el-col class="assertion-btn">

View File

@ -7,12 +7,14 @@
size="small"> size="small">
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/> <el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/> <el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
<el-option :label="'JSONPath'" :value="options.JSON_PATH"/>
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/> <el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/>
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="20"> <el-col :span="20">
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/> <ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/> <ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration" <ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/> v-if="type === options.DURATION" :callback="after"/>
<el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button> <el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button>
@ -30,11 +32,14 @@
import MsApiAssertionDuration from "./ApiAssertionDuration"; import MsApiAssertionDuration from "./ApiAssertionDuration";
import {ASSERTION_TYPE, Assertions} from "../../model/ScenarioModel"; import {ASSERTION_TYPE, Assertions} from "../../model/ScenarioModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit"; import MsApiAssertionsEdit from "./ApiAssertionsEdit";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
export default { export default {
name: "MsApiAssertions", name: "MsApiAssertions",
components: {MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText}, components: {
MsApiAssertionJsonPath,
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
props: { props: {
assertions: Assertions, assertions: Assertions,

View File

@ -9,6 +9,15 @@
</div> </div>
</div> </div>
<div class="assertion-item-editing json_path" v-if="assertions.jsonPath.length > 0">
<div>
{{'JSONPath'}}
</div>
<div class="regex-item" v-for="(jsonPath, index) in assertions.jsonPath" :key="index">
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" :json-path="jsonPath" :edit="true" :index="index"/>
</div>
</div>
<div class="assertion-item-editing response-time" v-if="isShow"> <div class="assertion-item-editing response-time" v-if="isShow">
<div> <div>
{{$t("api_test.request.assertions.response_time")}} {{$t("api_test.request.assertions.response_time")}}
@ -23,11 +32,12 @@
import MsApiAssertionRegex from "./ApiAssertionRegex"; import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionDuration from "./ApiAssertionDuration"; import MsApiAssertionDuration from "./ApiAssertionDuration";
import {Assertions} from "../../model/ScenarioModel"; import {Assertions} from "../../model/ScenarioModel";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
export default { export default {
name: "MsApiAssertionsEdit", name: "MsApiAssertionsEdit",
components: {MsApiAssertionDuration, MsApiAssertionRegex}, components: {MsApiAssertionJsonPath, MsApiAssertionDuration, MsApiAssertionRegex},
props: { props: {
assertions: Assertions, assertions: Assertions,
@ -56,6 +66,10 @@
border-left: 2px solid #7B0274; border-left: 2px solid #7B0274;
} }
.assertion-item-editing.json_path {
border-left: 2px solid #44B3D2;
}
.assertion-item-editing.response-time { .assertion-item-editing.response-time {
border-left: 2px solid #DD0240; border-left: 2px solid #DD0240;
} }

View File

@ -11,7 +11,7 @@
@change="change" show-word-limit :placeholder="$t('api_test.variable_name')"/> @change="change" show-word-limit :placeholder="$t('api_test.variable_name')"/>
</el-col> </el-col>
<el-col> <el-col>
<el-input :disabled="isReadOnly" v-model="common.expression" maxlength="255" size="small" show-word-limit <el-input :disabled="isReadOnly" v-model="common.expression" maxlength="2000" size="small" show-word-limit
:placeholder="expression"/> :placeholder="expression"/>
</el-col> </el-col>
<el-col class="extract-btn"> <el-col class="extract-btn">

View File

@ -352,6 +352,20 @@ export class ResponseAssertion extends DefaultTestElement {
} }
} }
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 { export class ResponseCodeAssertion extends ResponseAssertion {
constructor(testName, type, value, message) { constructor(testName, type, value, message) {
let assertion = { let assertion = {

View File

@ -12,7 +12,7 @@ import {
ResponseCodeAssertion, ResponseCodeAssertion,
ResponseDataAssertion, ResponseDataAssertion,
ResponseHeadersAssertion, ResponseHeadersAssertion,
RegexExtractor, JSONPostProcessor, XPath2Extractor, DubboSample, RegexExtractor, JSONPostProcessor, XPath2Extractor, DubboSample, JSONPathAssertion,
} from "./JMX"; } from "./JMX";
export const uuid = function () { export const uuid = function () {
@ -47,6 +47,7 @@ export const BODY_FORMAT = {
export const ASSERTION_TYPE = { export const ASSERTION_TYPE = {
TEXT: "Text", TEXT: "Text",
REGEX: "Regex", REGEX: "Regex",
JSON_PATH: "JSON",
DURATION: "Duration" DURATION: "Duration"
} }
@ -358,12 +359,6 @@ export class DubboRequest extends Request {
info: 'api_test.request.dubbo.input_method' info: 'api_test.request.dubbo.input_method'
} }
} }
if (!this.configCenter.isValid()) {
return {
isValid: false,
info: 'api_test.request.dubbo.input_config_center'
}
}
if (!this.registryCenter.isValid()) { if (!this.registryCenter.isValid()) {
return { return {
isValid: false, isValid: false,
@ -514,10 +509,11 @@ export class Assertions extends BaseConfig {
super(); super();
this.text = []; this.text = [];
this.regex = []; this.regex = [];
this.jsonPath = [];
this.duration = undefined; this.duration = undefined;
this.set(options); this.set(options);
this.sets({text: Text, regex: Regex}, options); this.sets({text: Text, regex: Regex, jsonPath: JSONPath}, options);
} }
initOptions(options) { initOptions(options) {
@ -560,6 +556,21 @@ export class Regex extends AssertionType {
} }
} }
export class JSONPath extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.JSON_PATH);
this.expression = undefined;
this.expect = undefined;
this.description = undefined;
this.set(options);
}
isValid() {
return !!this.expression;
}
}
export class Duration extends AssertionType { export class Duration extends AssertionType {
constructor(options) { constructor(options) {
super(ASSERTION_TYPE.DURATION); super(ASSERTION_TYPE.DURATION);
@ -886,7 +897,13 @@ class JMXGenerator {
let assertions = request.assertions; let assertions = request.assertions;
if (assertions.regex.length > 0) { if (assertions.regex.length > 0) {
assertions.regex.filter(this.filter).forEach(regex => { assertions.regex.filter(this.filter).forEach(regex => {
httpSamplerProxy.put(this.getAssertion(regex)); httpSamplerProxy.put(this.getResponseAssertion(regex));
})
}
if (assertions.jsonPath.length > 0) {
assertions.jsonPath.filter(this.filter).forEach(item => {
httpSamplerProxy.put(this.getJSONPathAssertion(item));
}) })
} }
@ -896,7 +913,12 @@ class JMXGenerator {
} }
} }
getAssertion(regex) { getJSONPathAssertion(jsonPath) {
let name = jsonPath.description;
return new JSONPathAssertion(name, jsonPath);
}
getResponseAssertion(regex) {
let name = regex.description; let name = regex.description;
let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则 let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则
let value = regex.expression; let value = regex.expression;

View File

@ -106,10 +106,10 @@
rules: { rules: {
name: [ name: [
{required: true, message: this.$t('project.input_name'), trigger: 'blur'}, {required: true, message: this.$t('project.input_name'), trigger: 'blur'},
{min: 2, max: 25, message: this.$t('commons.input_limit', [2, 25]), trigger: 'blur'} {min: 2, max: 50, message: this.$t('commons.input_limit', [2, 50]), trigger: 'blur'}
], ],
description: [ description: [
{max: 50, message: this.$t('commons.input_limit', [0, 50]), trigger: 'blur'} {max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'}
], ],
}, },
} }

View File

@ -28,8 +28,8 @@
<el-dialog :title="$t('member.create')" :visible.sync="createVisible" width="30%" :destroy-on-close="true" <el-dialog :title="$t('member.create')" :visible.sync="createVisible" width="30%" :destroy-on-close="true"
@close="handleClose"> @close="handleClose">
<el-form :model="form" ref="form" :rules="rules" label-position="right" label-width="100px" size="small"> <el-form :model="form" ref="form" :rules="rules" label-position="right" label-width="100px" size="small">
<el-form-item :label="$t('commons.member')" prop="ids"
<el-form-item :label="$t('commons.member')" prop="ids" :rules="{required: true, message: $t('member.input_id_or_email'), trigger: 'blur'}"> :rules="{required: true, message: $t('member.input_id_or_email'), trigger: 'blur'}">
<el-select <el-select
v-model="form.ids" v-model="form.ids"
multiple multiple
@ -51,9 +51,9 @@
<span class="org-member-email">{{item.email}}</span> <span class="org-member-email">{{item.email}}</span>
</template> </template>
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.role')" prop="roleIds"> <el-form-item :label="$t('commons.role')" prop="roleIds">
<el-select v-model="form.roleIds" multiple :placeholder="$t('role.please_choose_role')" class="select-width"> <el-select v-model="form.roleIds" multiple :placeholder="$t('role.please_choose_role')" class="select-width">
<el-option <el-option
@ -65,6 +65,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-form> </el-form>
<template v-slot:footer> <template v-slot:footer>
<ms-dialog-footer <ms-dialog-footer
@cancel="createVisible = false" @cancel="createVisible = false"
@ -87,7 +88,8 @@
<el-form-item :label="$t('commons.phone')" prop="phone"> <el-form-item :label="$t('commons.phone')" prop="phone">
<el-input v-model="form.phone" autocomplete="off" :disabled="true"/> <el-input v-model="form.phone" autocomplete="off" :disabled="true"/>
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.role')" prop="roleIds" :rules="{required: true, message: $t('role.please_choose_role'), trigger: 'change'}"> <el-form-item :label="$t('commons.role')" prop="roleIds"
:rules="{required: true, message: $t('role.please_choose_role'), trigger: 'change'}">
<el-select v-model="form.roleIds" multiple :placeholder="$t('role.please_choose_role')" class="select-width"> <el-select v-model="form.roleIds" multiple :placeholder="$t('role.please_choose_role')" class="select-width">
<el-option <el-option
v-for="item in form.allroles" v-for="item in form.allroles"
@ -127,7 +129,6 @@
result: {}, result: {},
createVisible: false, createVisible: false,
updateVisible: false, updateVisible: false,
userList: [],
form: {}, form: {},
queryPath: "/user/org/member/list", queryPath: "/user/org/member/list",
condition: {}, condition: {},
@ -146,7 +147,6 @@
total: 0, total: 0,
options: [], options: [],
loading: false, loading: false,
ids: []
} }
}, },
methods: { methods: {
@ -184,7 +184,7 @@
let roleIds = this.form.roles.map(r => r.id); let roleIds = this.form.roles.map(r => r.id);
this.result = this.$get('/role/list/org', response => { this.result = this.$get('/role/list/org', response => {
this.$set(this.form, "allroles", response.data); this.$set(this.form, "allroles", response.data);
}) });
// 使 // 使
this.$set(this.form, 'roleIds', roleIds); this.$set(this.form, 'roleIds', roleIds);
}, },
@ -196,7 +196,7 @@
phone: this.form.phone, phone: this.form.phone,
roleIds: this.form.roleIds, roleIds: this.form.roleIds,
organizationId: this.currentUser().lastOrganizationId organizationId: this.currentUser().lastOrganizationId
} };
this.$refs[formName].validate((valid) => { this.$refs[formName].validate((valid) => {
if (valid) { if (valid) {
this.result = this.$post("/organization/member/update", param, () => { this.result = this.$post("/organization/member/update", param, () => {
@ -229,9 +229,6 @@
} }
this.form = {}; this.form = {};
this.createVisible = true; this.createVisible = true;
// this.result = this.$get('/user/list/', response => {
// this.userList = response.data;
// });
this.result = this.$get('/role/list/org', response => { this.result = this.$get('/role/list/org', response => {
this.$set(this.form, "roles", response.data); this.$set(this.form, "roles", response.data);
}) })
@ -256,7 +253,7 @@
}); });
}, },
remoteMethod(query) { remoteMethod(query) {
query = query.trim() query = query.trim();
if (query !== '') { if (query !== '') {
this.loading = true; this.loading = true;
setTimeout(() => { setTimeout(() => {
@ -285,10 +282,6 @@
font-size: 13px; font-size: 13px;
} }
.input-with-autocomplete {
width: 100%;
}
.select-width { .select-width {
width: 100%; width: 100%;
} }

View File

@ -175,7 +175,7 @@
icon="el-icon-delete" icon="el-icon-delete"
circle size="mini" circle size="mini"
@click="handleDeleteStep(scope.$index, scope.row)" @click="handleDeleteStep(scope.$index, scope.row)"
:disabled="readOnly || scope.$index == 0 ? true : false"></el-button> :disabled="readOnly || (scope.$index == 0 && form.steps.length <= 1)"></el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@ -404,6 +404,7 @@ export default {
start_with: "Start with", start_with: "Start with",
end_with: "End With", end_with: "End With",
value: "Value", value: "Value",
expect: "Expect Value",
expression: "Expression", expression: "Expression",
response_in_time: "Response in time", response_in_time: "Response in time",
}, },

View File

@ -402,6 +402,7 @@ export default {
start_with: "以...开始", start_with: "以...开始",
end_with: "以...结束", end_with: "以...结束",
value: "值", value: "值",
expect: "期望值",
expression: "Perl型正则表达式", expression: "Perl型正则表达式",
response_in_time: "响应时间在...毫秒以内", response_in_time: "响应时间在...毫秒以内",
}, },

View File

@ -403,6 +403,7 @@ export default {
start_with: "以…開始", start_with: "以…開始",
end_with: "以…結束", end_with: "以…結束",
value: "值", value: "值",
expect: "期望值",
expression: "Perl型規則運算式", expression: "Perl型規則運算式",
response_in_time: "回應時間在…毫秒以內", response_in_time: "回應時間在…毫秒以內",
}, },